﻿/**
 * ColorMunch 1.0: AS3 library for the Adobe Kuler API (http://code.google.com/p/colormunch/)
 *
 * ColorMunch is (c) 20009 Ben Kanizay
 * This software is released under the MIT License (http://www.opensource.org/licenses/mit-license.php)
 *
 */

/**
 * @author Ben Kanizay (http://beekay.biz)
 */

package beekay.colormunch {
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;

	/**
	 * Manages communication with the Adobe Kuler API
	 * 
	 */
	public class ColorMunch extends EventDispatcher {

		/**
		* The API url for retrieving a themes list 
		*/
		public static const LIST_URL:String = "http://kuler-api.adobe.com/feeds/rss/get.cfm";
		/**
		* The API url for searching
		*/
		public static const SEARCH_URL:String = "http://kuler-api.adobe.com/rss/search.cfm";
		/**
		* The API url for retrieving a comments
		*/
		public static const COMMENTS_URL:String = "http://kuler-api.adobe.com/rss/comments.cfm";
		/**
		* The namespace value in the result XML
		*/
		public static const KULER_NAMESPACE:String = "http://kuler.adobe.com/kuler/API/rss/";


		/**
		* List type for retrieving the highest rated themes
		*/
		public static const LIST_HIGHEST_RATED:String = "rating";
		/**
		* List type for retrieving the mos recent themes
		*/
		public static const LIST_MOST_RECENT:String = "recent";
		/**
		* List type for retrieving random themes
		*/
		public static const LIST_RANDOM:String = "random";
		/**
		* List type for retrieving the most popular themes
		*/
		public static const LIST_POPULAR:String = "popular";


		/**
		* Clears the search filter
		*/
		public static const FILTER_NONE:String = "";
		/**
		* themeID search filter
		*/
		public static const FILTER_THEME_ID:String = "themeID";
		/**
		* userID search filter
		*/
		public static const FILTER_USER_ID:String = "userID";
		/**
		* email search filter
		*/
		public static const FILTER_EMAIL:String = "email";
		/**
		* tag search filter
		*/
		public static const FILTER_TAG:String = "tag";
		/**
		* hex search filter
		*/
		public static const FILTER_HEX:String = "hex";
		/**
		* Theme title search filter
		*/
		public static const FILTER_TITLE:String = "title";

		
		/**
		* search type for retrieving comments by themeID
		*/
		public static const COMMENTS_BY_THEME_ID:String = "themeID";
		/**
		* search type for retrieving comments by author email
		*/
		public static const COMMENTS_BY_EMAIL:String = "email";


		/**
		* default itemsPerPage value
		*/
		public static const ITEMS_PER_PAGE:int = 20;
		/**
		* default startIndex value
		*/
		public static const START_INDEX:int = 0;
		/**
		* default timeSpan value
		*/
		public static const TIME_SPAN:int = 0;

		
		/**
		* The Kuler namespace used in the result XML
		*/
		private var kuler:Namespace = new Namespace(KULER_NAMESPACE);
		/**
		* RegExp pattern for validating email address
		*/
		private var emailRegExp:RegExp = /^[A-Za-z0-9](([_\.\-]?[a-zA-Z0-9]+)+)@(([A-Za-z0-9]+)(([\.\-]?[a-zA-Z0-9]+)*)){2,}\.([A-Za-z]){2,4}+$/g;
		/**
		* RegExp pattern for matching a hex color in the format 0xFFFFFF or FFFFFF
		*/
		private var hexRegExp:RegExp = /\b0x(?:[0-9A-Fa-f]{6})\b/g;
		/**
		* RegExp pattern for matching themeId and userId (both are 6 digit values)
		*/
		private var themeUserIdRegExp:RegExp = /[0-9]{6}/g;
		/**
		* RegExp pattern for matching all html tags
		*/
		private var htmlTagsRegExp:RegExp = /<.*?>/g;
		/**
		* Array to store Theme objects from the themes result XML
		*/
		private var themes:Array = new Array();
		/**
		* Array to store Comment objects from the comments result XML
		*/
		private var comments:Array = new Array();
		/**
		* The Boolean value for whether an API call is currently in progress already
		*/
		private var busy:Boolean = false;
		/**
		* Our rss url loader
		*/
		private var rssLoader:URLLoader;
		/**
		* Holds the Adobe Kuler developer API key
		*/
		private var apiKey:String;
		/**
		* The error message as created by the query validation methods
		*/
		private var queryError:String;



		/**
		 * Crate a new ColorMunch instance
		 * 
		 * @param apiKey:String Your Adobe Kuler developer API key
		 * 
		 * @constructor
		 * 
		 */
		public function ColorMunch(apiKey:String) {
			this.apiKey = apiKey;
		}

		
		/**
		 * Starts the process of loading a Kuler themes list
		 * <p>The following events may dispatched during the process<ul><li>ColorMunchEvent.ERROR : The list type is invalid. See event.data in your event handler for detail</li><li>ColorMunchEvent.BUSY : This ColourFeed instance is already processing a request</li><li>ColorMunchEvent.LOAD_ERROR : A problem was encountered loading the feed</li><li>ColorMunchEvent.RESULT_EMPTY : There were zero results returned</li><li>ColorMunchEvent.RESULT_READY : The result has been parsed and the can now to be read (via the explicit getters)</li></ul></p>
		 * 
		 * @param listType (optional) The type of list to return. <p>Options are<ul><li>ColorMunch.LIST_MOST_RECENT (default - Get most recent themes)</li><li>ColorMunch.LIST_POPULAR (Get most popular themes)</li><li>ColorMunch.LIST_HIGHEST_RATED (Get highest-rated themes)</li><li>ColorMunch.LIST_RANDOM (Get random themes)</li></ul></p>
		 * @param startIndex (optional) A 0-based index into the list that specifies the first item to display. Default is 0, which displays the first item in the list.
		 * @param timeSpan (optional) Value in days to limit the set of themes retrieved. Default is 0, which retrieves all themes without time limit.
 		 * @param itemsPerPage (optional) The maximum number of items to display, in the range 1..100. Default is 20.
		 * 
		 */
		public function loadThemes(listType:String = LIST_MOST_RECENT, startIndex:int = START_INDEX, timeSpan:int = TIME_SPAN, itemsPerPage:int = ITEMS_PER_PAGE) : void {
			if (this.isValidListType(listType)) {
				var url:String = LIST_URL + "?listType=" + listType + "&startIndex=" + startIndex + "&timeSpan=" + timeSpan + "&itemsPerPage=" + itemsPerPage + "&key=" + this.apiKey;
				this.getThemesFeed(url);
			}
			else {
				this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.ERROR, "loadThemes error: Invalid list type"));
			}
		}

		/**
		 * Starts the process of searching Kuler themes
		 * <p>The following events may dispatched during the process<ul><li>ColorMunchEvent.ERROR : The list type is invalid. See event.data in your event handler for detail</li><li>ColorMunchEvent.BUSY : This ColourFeed instance is already processing a request</li><li>ColorMunchEvent.LOAD_ERROR : A problem was encountered loading the feed</li><li>ColorMunchEvent.RESULT_EMPTY : There were zero results returned</li><li>ColorMunchEvent.RESULT_READY : The result has been parsed and the can now to be read (via the explicit getters)</li></ul></p>
		 * 
		 * @param filter The filter to narrow your search. <p>Options are<ul><li>ColorMunch.FILTER_NONE (No filter. Will perform search on theme titles, tags, author names, themeIDs, authorIDs, and hexValues)</li><li>ColorMunch.FILTER_THEME_ID (Search on a specific themeID)</li><li>ColorMunch.FILTER_USER_ID (Search on a specific userID)</li><li>ColorMunch.FILTER_EMAIL (Search on a specific email)</li><li>ColorMunch.FILTER_TAG (Search on a tag word)</li><li>ColorMunch.FILTER_HEX (Search on a hex colour value - can be in the format "ABCDEF" or "0xABCDEF")</li><li>ColorMunch.FILTER_TITLE (Search on a theme title)</li></ul></p>
		 * @param query The search query. Use a simple string term to search on;
		 * @param startIndex (optional) A 0-based index into the list that specifies the first item to display. Default is 0, which displays the first item in the list.
 		 * @param itemsPerPage (optional) The maximum number of items to display, in the range 1..100. Default is 20.
		 * 
		 */
		public function searchThemes(filter:String, query:String, startIndex:int = START_INDEX, itemsPerPage:int = ITEMS_PER_PAGE) : void {
			if (this.isValidThemeSearch(filter, query)) {
				var searchQuery:String = (filter == FILTER_NONE) ? query : filter + ":" + query;
				var url:String = SEARCH_URL + "?searchQuery=" + searchQuery + "&startIndex=" + startIndex + "&itemsPerPage=" + itemsPerPage + "&key=" + this.apiKey;
				this.getThemesFeed(url);
			}
			else {
				this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.ERROR, "searchThemes error: " + this.queryError));
			}
		}
		

		/**
		 * Starts the process of loading Kuler theme comments
		 * <p>The following events may dispatched during the process<ul><li>ColorMunchEvent.ERROR : The list type is invalid. See event.data in your event handler for detail</li><li>ColorMunchEvent.BUSY : This ColourFeed instance is already processing a request</li><li>ColorMunchEvent.LOAD_ERROR : A problem was encountered loading the feed</li><li>ColorMunchEvent.RESULT_EMPTY : There were zero results returned</li><li>ColorMunchEvent.RESULT_READY : The result has been parsed and the can now to be read (via the explicit getters)</li></ul></p>
		 * 
		 * @param filter The filter to search by. <p>Options are<ul><li>ColorMunch.COMMENTS_BY_EMAIL (comments are retrieved for themes created by this user)</li><li>ColorMunch.COMMENTS_BY_THEME_ID (comments are retrieved for the specified theme)</li></ul></p>
		 * @param query The value associated with the filter. If the filter is ColorMunch.COMMENTS_BY_EMAIL then query would be a users' email address, else if the filter is ColorMunch.COMMENTS_BY_THEME_ID then the query would be a themeID.
		 * @param startIndex (optional) A 0-based index into the list that specifies the first item to display. Default is 0, which displays the first item in the list.
 		 * @param itemsPerPage (optional) The maximum number of items to display, in the range 1..100. Default is 20.
		 * 
		 */
		public function loadComments(filter:String, query:String, startIndex:int = START_INDEX, itemsPerPage:int = ITEMS_PER_PAGE) : void {
			if (this.isValidCommentSearch(filter, query)) {
				var url:String = ColorMunch.COMMENTS_URL + "?" + filter + "=" + query + "&startIndex=" + startIndex + "&itemsPerPage=" + itemsPerPage + "&key=" + this.apiKey;			
				this.getCommentsFeed(url);
			}
			else {
				this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.ERROR, "loadComments error: " + this.queryError));
			}
		}


		/**
		 * Loads a themes feed as called by either loadThemes or searchThemes
		 * 
		 * @param url The full feed/query url
		 * 
		 */
		private function getThemesFeed(url:String) : void {
			if (this.busy) {
				this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.BUSY, null));
			}
			else {
				this.busy = true;
				this.rssLoader = new URLLoader();	
				this.rssLoader.addEventListener(IOErrorEvent.IO_ERROR, this.onThemesLoadError);
				this.rssLoader.addEventListener(Event.COMPLETE, this.onThemesLoaded);
				var safeUrl:String = url.replace(this.htmlTagsRegExp, "");
				try {
					this.rssLoader.load(new URLRequest(safeUrl));
				}
				catch (error : Error) {
					this.busy = false;
					this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.LOAD_ERROR, error.message));
				}
			}
		}


		/**
		 * Loads a comments feed as called by loadComments
		 * 
		 * @param url The full feed/query url
		 * 
		 */
		private function getCommentsFeed(url:String) : void {
			if (this.busy) {
				this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.BUSY, null));
			}
			else {
				this.busy = true;
				this.rssLoader = new URLLoader();	
				this.rssLoader.addEventListener(IOErrorEvent.IO_ERROR, this.onCommentsLoadError);
				this.rssLoader.addEventListener(Event.COMPLETE, this.onCommentsLoaded);
				var safeUrl:String = url.replace(this.htmlTagsRegExp, "");
				try {
					this.rssLoader.load(new URLRequest(safeUrl));
				}
				catch (error : Error) {
					this.busy = false;
					this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.LOAD_ERROR, error.message));
				}
			}
		}


		/**
		 * IOErrorEvent.IO_ERROR event handler
		 * 
		 * @param event
		 * 
		 */
		private function onThemesLoadError(event:IOErrorEvent) : void {
			this.rssLoader.removeEventListener(Event.COMPLETE, this.onThemesLoaded);
			this.rssLoader.removeEventListener(IOErrorEvent.IO_ERROR, this.onThemesLoadError);
			this.busy = false;
			this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.LOAD_ERROR, event.text));
		}


		/**
		 * Event.COMPLETE event handler
		 * 
		 * @param event
		 * 
		 */
		private function onThemesLoaded(event:Event) : void {
			this.rssLoader.removeEventListener(Event.COMPLETE, this.onThemesLoaded);
			this.rssLoader.removeEventListener(IOErrorEvent.IO_ERROR, this.onThemesLoadError);
			var xml:XML = new XML(this.rssLoader.data);
			var themesXML:XMLList = new XMLList(xml.channel.item);
			this.parseThemesXML(themesXML);
		}


		/**
		 * IOErrorEvent.IO_ERROR event handler
		 * 
		 * @param event
		 * 
		 */
		private function onCommentsLoadError(event:IOErrorEvent) : void {
			this.rssLoader.removeEventListener(Event.COMPLETE, this.onThemesLoaded);
			this.rssLoader.removeEventListener(IOErrorEvent.IO_ERROR, this.onThemesLoadError);
			this.busy = false;
			this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.LOAD_ERROR, event.text));
		}

		/**
		 * Event.COMPLETE event handler
		 * 
		 * @param event
		 * 
		 */
		private function onCommentsLoaded(event:Event) : void {
			this.rssLoader.removeEventListener(Event.COMPLETE, this.onCommentsLoaded);
			this.rssLoader.removeEventListener(IOErrorEvent.IO_ERROR, this.onCommentsLoadError);
			var xml:XML = new XML(this.rssLoader.data);
			var commentsXML:XMLList = new XMLList(xml.channel.item);
			this.parseCommentsXML(commentsXML);
		}


		/**
		 * 
		 * Parses the XMLList of themes from the feed result, creating a new Theme object for each and pushing those into the themes array
		 * <p>When the data has been parsed a ColorMunchEvent.RESULT_READY event is dispatched</p>
		 * <p>If the result is empty a ColorMunchEvent.RESULT_EMPTY event is dispatched</p>
		 * 
		 * @param themesXML The XMLList of Kuler themes to parse
		 * 
		 */
		private function parseThemesXML(themesXML:XMLList) : void {
			var themeCount:int = themesXML.length();
			this.themes = [];
			if (themeCount > 0) {
				for (var i:int = 0; i < themeCount; i++) {
					var theme:XML = themesXML[i];
					this.themes.push(new Theme(
						theme.kuler::themeItem.kuler::themeID, 
						theme.kuler::themeItem.kuler::themeTitle, 
						theme.kuler::themeItem.kuler::themeImage, 
						theme.link, 
						theme.kuler::themeItem.kuler::themeCreatedAt, 
						theme.kuler::themeItem.kuler::themeEditedAt, 
						theme.kuler::themeItem.kuler::themeTags, 
						theme.kuler::themeItem.kuler::themeRating, 
						theme.kuler::themeItem.kuler::themeDownloadCount, 
						theme.kuler::themeItem.kuler::themeAuthor.kuler::authorID, 
						theme.kuler::themeItem.kuler::themeAuthor.kuler::authorLabel, 
						new XMLList(theme.kuler::themeItem.kuler::themeSwatches.kuler::swatch), 
						this.apiKey
					));
				}
				this.busy = false;
				this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.RESULT_READY, null));
			}
			else {
				this.busy = false;
				this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.RESULT_EMPTY, "0 themes found"));
			}
		}


		/**
		 * 
		 * Parses the XMLList of comments from the feed result, creating a new Comment object for each and pushing those into the themes array
		 * <p>When the data has been parsed a ColorMunchEvent.RESULT_READY event is dispatched</p>
		 * <p>If the result is empty a ColorMunchEvent.RESULT_EMPTY event is dispatched</p>
		 * 
		 * @param commentsXML The XMLList of Kuler theme comments to parse
		 * 
		 */
		private function parseCommentsXML(commentsXML:XMLList) : void {
			var commentCount:int = commentsXML.length();
			this.comments = [];
			if (commentCount > 0) {
				for (var i:int = 0; i < commentCount; i++) {
					var comment:XML = commentsXML[i];
					this.comments.push(new Comment(
						comment.kuler::commentItem.kuler::comment, 
						comment.kuler::commentItem.kuler::author, 
						comment.kuler::commentItem.kuler::postedAt
					));
				}
				this.busy = false;
				this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.RESULT_READY, null));
			}
			else {
				this.busy = false;
				this.dispatchEvent(new ColorMunchEvent(ColorMunchEvent.RESULT_EMPTY, "0 comments found"));
			}
		}


		/**
		 * 
		 * An internal helper method to check that a valid list type is being used
		 * 
		 * @return A value of <code>true</code> means the list type is valid;
		 * A value of <code>false</false> means the list type string is invalid
		 * 
		 * @see loadThemes
		 * 
		 */
		private function isValidListType(listType:String) : Boolean {
			var valid:Boolean = false;
			switch(listType) {
				case LIST_HIGHEST_RATED:
				case LIST_MOST_RECENT:
				case LIST_POPULAR:
				case LIST_RANDOM:
					valid = true;
					break;
			}
			return valid;
		}


		/**
		 * 
		 * An internal helper method to check that a valid theme search filter/query pair is being used
		 * 
		 * @return <code>true</code> means it is valid, <code>false</false> means it is invalid
		 * 
		 * @see searchThemes
		 * 
		 */
		private function isValidThemeSearch(filter:String, query:String) : Boolean {
			var valid:Boolean;
			var error:String;
			switch(filter) {
				case FILTER_THEME_ID :
					valid = (this.themeUserIdRegExp.test(query));
					error = "Invalid themeID. Must contain 6 digits";
					break;
				case FILTER_USER_ID :
					valid = (this.themeUserIdRegExp.test(query));
					error = "Invalid userID. Must contain 6 digits";
					break;
				case FILTER_EMAIL :
					valid = (this.emailRegExp.test(query));
					error = "Invalid email address";
					break;
				case FILTER_HEX :
					valid = (this.hexRegExp.test(query));
					error = "Invalid hex value. Must be in the format 'ABCDEF' or '0xABCDEF'";
					break;
				case FILTER_TAG :
				case FILTER_NONE :
				case FILTER_TITLE :
					valid = true;
					break;
				default :
					valid = false;
					error = "Invalid search filter";
					break;
			}
			this.queryError = (valid == true) ? "" : error;
			return valid;
		}


		/**
		 * 
		 * An internal helper method to check that a valid comment search filter/query pair is being used
		 * 
		 * @return <code>true</code> means it is valid, <code>false</false> means it is invalid
		 * 
		 * @see loadComments
		 * 
		 */
		private function isValidCommentSearch(filter:String, query:String) : Boolean {
			var valid:Boolean;
			var error:String;
			switch(filter) {
				case COMMENTS_BY_EMAIL :
					valid = (this.emailRegExp.test(query));
					error = "Invalid email address";
					break;
				case COMMENTS_BY_THEME_ID :
					valid = (this.themeUserIdRegExp.test(query));
					error = "Invalid themeID. Must contain 6 digits";
					break;
				default :
					valid = false;
					error = "Invalid comment search filter";
					break;
			}
			this.queryError = (valid == true) ? "" : error;
			return valid;
		}




		/**
		 * Get the total number of themes in the feed result
		 * 
		 * @return The total number of themes
		 * 
		 */
		public function getThemeCount() : int {
			return this.themes.length;
		}

		/**
		 * Get all themes from the feed result
		 * 
		 * @return An array of themes. Each array item is of type Theme
		 * 
-		 */
		public function getAllThemes() : Array {
			return this.themes;
		}

		/**
		 * Get a specific Theme from the feed result
		 * 
		 * @param index The array index of the theme
		 * 
		 * @return The requested theme
		 * 
		 */
		public function getThemeByIndex(index:int) : Theme {
			return this.themes[index];
		}

		/**
		 * Get a random theme from the feed result
		 * 
		 * @return A random theme
		 * 
		 */
		public function getRandomTheme() : Theme {
			var random:int =  Math.floor(Math.random() * (this.themes.length));
			return this.themes[random];
		}




		/**
		 * Get the total number of comments in the feed result
		 * 
		 * @return The total number of comments
		 * 
		 */
		public function getCommentCount() : int {
			return this.comments.length;
		}

		/**
		 * Get all comments from the feed result
		 * 
		 * @return An array of comments. Each array item is of type Comment
		 * 
-		 */
		public function getAllComments() : Array {
			return this.comments;
		}

		/**
		 * Get a specific Comment from the feed result
		 * 
		 * @param index The array index of the comment
		 * 
		 * @return The requested comment
		 * 
		 */
		public function getCommentByIndex(index:int) : Comment {
			return this.comments[index];
		}

		/**
		 * Get a random comment from the feed result
		 * 
		 * @return A random comment
		 * 
		 */
		public function getRandomComment() : Comment {
			var random:int =  Math.floor(Math.random() * (this.comments.length));
			return this.comments[random];
		}

	}
}